CSRF 공격 방지와 csurf middleware
✒️ 2025-05-26 14:11 내용 수정
Node.js 교과서 개정 3판 내용 정리
CSRF(Cross Site Request Forgery)
사용자가 의도치 않게 공격자가 의도한 행동을 하게 만드는 공격
- cookie를 통한 인증 방식에서 사용되는 공격이다.
- CSRF(Cross Site Request Forgery) 참고.
1. cookie 속성 중 sameSite 사용
- 참고 자료 : mdn web docs Set-Cookie, stackoverflow What is the difference between SameSite="Lax" and SameSite="Strict"
- cookie가 cross-site 요청과 보내졌는지 여부를 판단해 CSRF 공격을 방지하도록 설정할 수 있다.
Strict: 브라우저가 same-site 요청을 위한 cookie만 보내므로 다른 도메인에 의한 cookie를 보내지 않는다.(cross-site 요청을 위한 cookie를 보내지 않음)Lax: 브라우저가 요청이 안전한 경우나 top-level navigation 요청의 경우에는 cross-site 요청을 위한 cookie를 보낼 수 있다.None: 브라우저가 cross-site와 same-site 요청을 위한 cookie를 모두 보낼 수 있다.
// express server
app.use(session({
secret: 'custom-cookie-secret',
cookie: {
sameSite: 'Strict' // 또는 'Lax'
}
}));
2. CORS 설정
- CORS 설정을 통해 특정 출처에서만 서버에 접근할 수 있도록 제한한다.
// express server
const cors = require('cors');
const corsOptions = {
origin: 'https://your-allowed-origin.com'
};
app.use(cors(corsOptions));
csurf middleware
CSRF 공격을 막기 위한 패키지
- 공식 문서 : https://www.npmjs.com/package/csrf
- 해당 미들웨어는 내가 한 행동이 맞다는 것을 인증하기 위해 CSRF 토큰을 사용한다.
- cookie를 사용하도록 설정한다면
cookie-parser도 사용 설정을 해야 하며, 이 사용 전에 해당 코드를 작성해야 한다.- cookie-parser 참고.
- 사용하지 않는 경우라면 sesssion middleware가 필요하다.(express-session, cookie-session)
- cookie를 사용하도록 설정한다면
npm install csurf
- cookie를 사용하지 않거나 JWT를 사용하는 경우엔 따로 설정이 필요 없다는 의견들이 있었다. 아직 이 부분에 대한 이해가 부족해서 사이트들의 링크를 두고 서비스에 따라 필요 여부를 확인하여 반영해야 할 듯 하다.
참고 자료 : reddit Should I deploy CSRF token for react SPA?, reddit How do you protect against CSRF attacks in a react app?, stackoverflow CSRF Token necessary when using Stateless(=Sessionless) Authentication?
1. 기본 예시
- 예시 코드는 공식 문서의 내용을 그대로 가져왔다.
- csrfToken을 생성하고, 특정 라우터에서 렌더링 시에 token을 함께 보내도록 설정한다.
cookie-parser나express-session을 사용해야 하므로 두 middleware보다 아래에 위치해야 한다.
// server.js
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const express = require('express');
const app = express();
const csrfProtection = csrf({ cookie: true }); // cookie-parser 필요
const parseFomr = bodyParser.urlencoded({ extended : false });
// cookie-parser 사용
app.use(cookieParser());
// 미들웨어 형식의 동작
// /test라는 라우터로 접근하면 토큰 발행
app.get('/test', csrfProtection, (req, res) => {
res.render('csrf', { csrfToken: req.csrfToken() });
});
// form에서 보낸 데이터를 처리하는 라우터
// 프론트에서 렌더링된 CSRF 토큰을 form 제출 시에 같이 제출해야 함
app.post('/test', csrfProtection, (req, res) => {
res.send('csrf-ok');
});
- 프론트에선
<form>태그에서 CSRF 토큰 값을 같이 넘겨주도록 설정한다.
<form action="/process" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<button type="submit">Submit</button>
</form>
2. AJAX 사용
- Request Header에 csrfToken을 담아 보내는 방식으로 처리할 수 있다.
- 이 경우 view 단에서 서버로부터 받은
req.csrfToken()을<meta>태그에 추가한다.
<meta name="csrf-token" content="{{csrfToken}}">
- 공식 문서 예시에선 Fetch API를 사용한 예시로 작성하였다.
- Fetch 참고.
/process라는 라우트로 접근할 때<meta>태그에 저장한 token을 가져와 request header에 추가한다.
// meta 태그에 저장된 token을 가져오기
var token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
// 요청 생성
fetch('/process', {
credentials: 'same-origin', // 요청에 cookie 추가
headers: {
'CSRF-Token': token // csrf token을 header에 추가
},
method: 'POST',
body: {
name : 'test'
}
});
3. React에서 Token을 받고 전송하기
- 예시 및 참고 자료들에선 서버에서 직접
res.render()등을 통해csrfToken()을 보내주었다. - 하지만 React와 같은 SPA에서는 페이지 라우트를 처리할 때
react-router와 같은 라이브러리를 통해 처리하면 서버에선 페이지를 전송하는 경우가 아니므로 이런 경우의 Token 발급 및 전송을 고민하게 되었다. - 챗GPT의 도움으로 서버(Node)와 React에서의 csrf 코드를 작성했다.
- React 페이지의 App.js가 처음 렌더링 될 때 csrfToken을 발급 받는 요청을 보내고, axios header에 csrfToken을 추가한다.
- 서버에선 모든 POST, PUT, DELETE 요청에 대해 csrfToken을 검증하고, 검증될 경우에 다음 동작을 수행하는 미들웨어를 거치도록 설정한다.
// server.js
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
// token 발행 요청
app.get('/csrf', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
const bodyParser = require('body-parser'); // form 데이터 파싱 설정
const parseForm = bodyParser.urlencoded({ extended: false });
// POST, PUT, DELETE 요청에 대해 csrfToken 검증
app.post('*', parseForm, csrfProtection, (req, res, next) => {
next();
});
app.put('*', parseForm, csrfProtection, (req, res, next) => {
next();
});
app.delete('*', parseForm, csrfProtection, (req, res, next) => {
next();
});
// App.js
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
function App() {
const navigate = useNavigate();
useEffect(()=>{
axios.get('/csrf')
.then(res=>{
// axios header를 설정하여 이후 모든 종류의 요청의 header에 csrfToken이 포함되도록 한다
axios.defaults.headers.common['X-CSRF-Token'] = res.data.csrfToken;
})
.catch(err=>{
navigate('/error/400'); // 에러가 생기면 에러 페이지로 보낸다
});
}, [navigate]);
return(</>)
}
- App.js가 렌더링되면 GET
/csrf요청을 보내 csrfToken을 받는다.
- 다른 요청을 확인해보면 Request Header에
X-Csrf-Token항목이 추가되어 있다.
- cookie 탭에서
_csrf라는 이름의 cookie가 있는데, 참고한 사이트에선 cookie secret을 cookie나 session에 저장한다고 나와있다.
- 설정 완료 후 POST, PUT, DELETE 요청을 수행했을 때 버그가 발생하지 않았다.
- 그래서 검증이 제대로 수행됬는지 확인하기 위해 Request Header에 포함될 csrfToken의 값을 임의로 수정하여 POST 동작을 실행했다.
// App.js
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
function App() {
const navigate = useNavigate();
useEffect(()=>{
axios.get('/csrf')
.then(res=>{
// 검증 확인을 위해 Header에 들어갈 csrfToken 값을 임의로 수정했다.
axios.defaults.headers.common['X-CSRF-Token'] = res.data.csrfToken + 'sld';
})
.catch(err=>{
navigate('/error/400');
});
}, [navigate]);
return(</>)
}
- 테스트를 위해 팀 프로젝트에서 POST 동작이 있는 페이지에서 동작을 테스트했다.
- 정보를 입력하고 버튼을 눌러 POST 동작을 요청했을 때 에러가 발생하면서 POST가 처리되지 않았다.
- 그리고 다시 상품 목록을 확인했을 때 방금 추가한 "에러테스트"라는 제목의 상품이 추가되지 않은 것을 확인했다.
- csrfToken 검증을 통과하지 못해 POST 동작 자체가 막힌 것을 확인할 수 있었다.